package Connections;

import ConnectionsCommands.ConnectionHashCommand;
import Gear.Factory;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Semaphore;

/**
 * Tracker (central server) which controls all file operations on network.
 *
 * @author Rodrigo Martins Racanicci
 */
public class Tracker extends Server {

    /**
     * Hash to deal with received commands.
     */
    private final ConnectionHashCommand commands;

    /**
     * Online peers list.
     */
    private final ArrayList<String> peers;

    /**
     * Peers local file on tracker.
     */
    private final File peersFile;

    /**
     * File to a peer that contains it hash.
     */
    private final HashMap<String, HashSet<String>> file2Peer;

    /**
     * Peer to a file that contains it hash.
     */
    private final HashMap<String, HashSet<String>> peer2File;

    /**
     * Semaphore to ensure race conditions on file operations.
     */
    private final Semaphore fileMutex;

    /**
     * Locked files which are being used by some peer.
     */
    private final ArrayList<String[]> lockedFiles;

    /**
     * Tracker constructor.
     *
     * @param name Tracker name.
     */
    public Tracker(String name) {
        super(name);
        commands = new ConnectionHashCommand();
        peers = new ArrayList<>();
        peersFile = FileSystems.getDefault().getPath(Factory.getInstance().getSettings().getGearPath().toString(), "peers.gear").toFile();
        file2Peer = new HashMap<>();
        peer2File = new HashMap<>();
        fileMutex = new Semaphore(1);
        lockedFiles = new ArrayList<>();


        try {
            peersFile.delete();
            peersFile.createNewFile();
        } catch (IOException ex) {
        }
    }

    /**
     * Check if a file is locked.
     *
     * @param fileName File to verify.
     *
     * @return true or false.
     */
    public boolean isLocked(String fileName) {
        for (String[] t : this.lockedFiles) {
            if (t[1].equals(fileName)) return true;
        }
        return false;
    }

    /**
     * Attempt to lock a file if it exists and is not being used.
     *
     * @param peer     Peer that contains the file.
     * @param fileName File name.
     *
     * @return true if success, false otherwise.
     */
    public boolean lockFile(String peer, String fileName) {

        for (String[] t : this.lockedFiles) {
            if (t[1].equals(fileName)) return false;
        }

        if (!peer2File.get(peer).contains(fileName)) return false;

        String[] tuple = new String[2];

        tuple[0] = peer;
        tuple[1] = fileName;

        this.lockedFiles.add(tuple);

        return true;
    }

    /**
     * Attempt to unlock a file if it exists and is being used.
     *
     * @param peer     Peer that contains the file.
     * @param fileName File name.
     *
     * @return true if success, false otherwise.
     */
    public boolean unlockFile(String peer, String fileName) {

        String[] tuple = new String[2];

        tuple[0] = peer;
        tuple[1] = fileName;

        Iterator t = lockedFiles.iterator();
        while (t.hasNext()) {
            if (Arrays.deepEquals((Object[]) t.next(), tuple)) {
                t.remove();
                return true;
            }
        }

        return false;
    }

    /**
     * Get peers that contains a specified file.
     *
     * @param fileName File name.
     *
     * @return Set of peers names.
     */
    public Set getPeersFromFile(String fileName) {
        return (Set<String>) file2Peer.get(fileName);
    }


    /**
     * Get a random peer which contains a specified file.
     *
     * @param fileName File name.
     *
     * @return Peer host.
     */
    public String requestPeer(String fileName) {

        if (file2Peer.containsKey(fileName)) {
            Random r = new Random();
            Object[] p = file2Peer.get(fileName).toArray();

            return (String) p[r.nextInt(p.length)];

        } else {
            return null;
        }
    }

    /**
     * Register a file on network.
     *
     * @param peer     Peer which contains the file.
     * @param fileName FileName.
     */
    public void addFile(String peer, String fileName) {
        try {
            file2Peer.get(fileName).add(peer);
        } catch (NullPointerException e) {
            file2Peer.put(fileName, new HashSet<String>());
            file2Peer.get(fileName).add(peer);
        }

        try {
            peer2File.get(peer).add(fileName);
        } catch (NullPointerException e) {
            peer2File.put(peer, new HashSet<String>());
            peer2File.get(peer).add(fileName);
        }
    }

    /**
     * Remove a peer file of the network.
     *
     * @param peer     Peer which contains the file.
     * @param fileName FileName.
     */
    public void removeFile(String peer, String fileName) {
        System.out.println("RM "+ peer + " " + fileName);
        file2Peer.get(fileName).remove(peer);
        peer2File.get(peer).remove(fileName);

        if (file2Peer.get(fileName).isEmpty()) {
            file2Peer.remove(fileName);
        }

        if (peer2File.get(peer).isEmpty()) {
            peer2File.remove(peer);
        }
    }

    /**
     * Remove a peer of the network.
     *
     * @param peer Peer host.
     */
    public void removeHost(String peer) {
        for (String f : peer2File.get(peer)) {
            file2Peer.get(f).remove(peer);
            if (file2Peer.get(f).isEmpty()) {
                file2Peer.remove(f);
            }
        }
        peer2File.remove(peer);
    }

    /**
     * Verify is exists a peer with a specified file
     * @param peer Peer host.
     * @param fileName File name.
     * @return true means success, false otherwise.
     */
    public boolean verifyFile (String peer, String fileName) {
        try {
            if (file2Peer.get(fileName).contains(peer) && peer2File.get(peer).contains(fileName))
                return true;
            else
                return false;
            
        } catch(Exception e) { return false; }
    }
    
    /**
     * Refresh local peers file.
     */
    public void updatePeersFile() {
        BufferedWriter writer = null;

        try {
            peersFile.delete();
            writer = new BufferedWriter(new FileWriter(peersFile));
            for (String peer : peers)
                writer.write(peer + "\n");
            writer.flush();
            writer.close();
        } catch (IOException ex) {
            System.out.println(name + ": error updating peers file");
        } finally {
            try {
                writer.close();
            } catch (IOException ex) {
                System.out.println(name + ": error closing peers file");
            }
        }

    }

    /**
     * Put tracker to accept connections.
     */
    public void acceptConnections() {
        Socket client;
        ConnectionHandler clientHandler;

        System.out.println(name + ": waiting for connections...");
        while (this.isAlive) {
            try {
                client = this.serverSocket.accept();
                //down
                clientHandler = new TrackerHandler(client);
                registerClient(client, clientHandler);
                clientHandler.start();
                System.out.println(name + ": new client at" + client.getInetAddress());
                //up
            } catch (IOException ex) {
                System.out.println(name + ": error accepting connection at in server socket");
            }
        }
    }

    /**
     * Get commands hash.
     *
     * @return Commands hash.
     */
    public ConnectionHashCommand getCommands() {
        return commands;
    }

    /**
     * Add a peer on network.
     *
     * @param host Peer host.
     */
    public void addPeer(String host) {
        if (!peers.contains(host)) {
            peers.add(host);
            //updatePeersFile();
        }
        System.out.println(name + ": new peer at " + host);
    }

    /**
     * Get peers local file.
     *
     * @return Peers local files.
     */
    public File getPeersFile() {
        return peersFile;
    }

    /**
     * Get the available peers on network.
     *
     * @return List of available peers.
     */
    public ArrayList<String> getPeers() {
        return peers;
    }

    /**
     * Get files available on network.
     *
     * @return List of available files.
     */
    public Set<String> getFileNames() {
        return file2Peer.keySet();
    }

    /**
     * Get file mutex.
     *
     * @return File mutex.
     */
    public Semaphore getFileMutex() {
        return fileMutex;
    }
}
